Day 23 寫到 Function declaration、function expression、arrow function。
寫著寫著,我發現一直有一個字在旁邊晃來晃去:
Scope
以前看到 Scope,我大概會翻成:
作用域。
然後就沒了。
但「作用域」三個字本身沒有讓我比較懂。
真正讓我卡住的是這種程式:
if (true) {
const count = 10
}
console.log(count)
為什麼前面明明有:
const count = 10
下面卻用不到?
變數是消失了嗎?
我現在會先把 Scope 想成:
一個名稱在哪些程式碼範圍內可以被找到。
例如:
const name = 'Anthony'
重點不只是「建立了一個值」。
還有:
name 這個名稱
在哪裡可以被查到?
所以 Scope 管的不是值有沒有存在,而是:
目前這段程式碼,有沒有資格用這個名字找到那個值?
function greet() {
const name = 'Anthony'
console.log(name)
}
greet()
這裡可以正常印出:
Anthony
但如果我改成:
function greet() {
const name = 'Anthony'
}
console.log(name)
就會出錯。
為什麼?
因為:
const name = 'Anthony'
建立在 greet 這個 function 的 Scope 裡。
所以 function 外面找不到 name。
我以前腦中會偷偷想成:
離開 function
↓
name 被刪除
但這其實把兩件事混在一起了。
Scope 先回答的是:
這裡能不能用 name 這個名字?
而不是:
那個值現在還存在記憶體嗎?
所以今天先不要把 Garbage Collection 也拉進來。
先只處理「名稱可見性」。
看這段:
if (true) {
const count = 10
console.log(count)
}
裡面可以用 count。
但外面:
if (true) {
const count = 10
}
console.log(count)
不行。
因為 let 和 const 有 Block Scope。
像這些大括號:
if (...) {
}
for (...) {
}
{
}
都可能形成 block。
所以:
{
const count = 10
}
可以先想成:
count 只活在這個名稱範圍裡
這時候就會發現 JavaScript 又不太整齊。
if (true) {
var count = 10
}
console.log(count)
這段反而可能可以拿到 count。
因為 var 不是 Block Scope。
它比較偏向 Function Scope。
這也是為什麼現在很多 JavaScript 程式會優先使用:
const
let
而不是一開始什麼都用 var。
不是因為 var 比較舊所以單純淘汰,而是它的 Scope 行為比較容易讓人踩坑。
看:
function test() {
var a = 1
const b = 2
let c = 3
}
a、b、c 都不能直接在 function 外面使用。
console.log(a)
console.log(b)
console.log(c)
都會有問題。
所以 Function 本身也會形成自己的 Scope。
目前可以先這樣分:
Function Scope
↓
function 裡的名稱範圍
Block Scope
↓
大括號區塊裡 let / const 的名稱範圍
再看:
const user = 'Anthony'
function greet() {
console.log(user)
}
greet()
greet 裡面沒有宣告 user。
為什麼還是找得到?
因為 JavaScript 找名稱時,會先看目前 Scope。
找不到,就往外層 Scope 找。
概念上像:
greet 的 Scope
↓
找 user
↓
沒有
↓
往外層找
↓
找到 user
這就是 Scope Chain 的基本畫面。
function greet() {
const secret = 'hello'
}
console.log(secret)
不行。
因為查找方向不是:
全世界都翻一遍
而是從目前位置往外找。
外層不會主動鑽進某個 function 裡面找名稱。
這樣也合理,不然每個 function 裡面的暫時變數都可能污染外面的世界。
看:
const name = 'Outside'
function greet() {
const name = 'Inside'
console.log(name)
}
greet()
結果會是:
Inside
因為 JavaScript 找 name 時,先看最近的 Scope。
最近已經找到:
const name = 'Inside'
就不需要再往外找。
這種內層名稱把外層同名名稱遮住的情況,叫:
shadowing
所以 Scope Chain 不只是「往外找」。
更完整一點是:
先找最近的
↓
找到就停
↓
找不到才往外
其實前面一直在用。
例如:
function Counter() {
const [count, setCount] = useState(0)
function handleClick() {
console.log(count)
}
return <button onClick={handleClick}>+</button>
}
handleClick 裡沒有重新宣告 count。
可是它看得到外層 Counter 裡的 count。
因為:
handleClick Scope
↓
沒有 count
↓
往外找
↓
Counter Scope
↓
找到 count
後面學 Closure 時,這條線會變得更重要。
以前會說:
這個變數是區域變數。
但其實我沒有真的想過什麼叫區域。
現在比較能拆成:
宣告發生在哪個 Scope
↓
決定名稱能在哪裡被找到
所以寫:
const total = 100
真正要問的不只是:
total 的值是多少?
還要問:
這個 total 是在哪一層 Scope 宣告的?
Scope 主要是在管名稱的可見範圍。
對 let / const 來說,Block Scope 很重要。
但 var 的行為不同。
不是。
名稱查找會從目前 Scope 往外走,不會反過來鑽進內層。
不一定。
不同 Scope 可以有同名 binding,內層會優先遮住外層。
Scope 決定的是:一個名稱在程式的哪些地方可以被找到。
而 JavaScript 查找名稱時:
目前 Scope
↓
找不到
↓
往外層 Scope
↓
再找
這條 Scope Chain 會一路影響後面的 Closure、Hoisting、this,甚至 React 裡的 callback 行為。
但現在又有一個更奇怪的事情。
sayHello()
function sayHello() {
console.log('Hello')
}
Function 明明寫在下面,為什麼上面可以先呼叫?
可是:
sayHello()
const sayHello = () => {
console.log('Hello')
}
卻不行。
很多教學會直接說:
因為 hoisting。
但 Hoisting 真的是 JavaScript 把程式碼搬到最上面嗎?
下一篇再說。